あなたのLambdaが動いているのはEC2の上?それともFirecrackerの上?
サーバーレス開発部@大阪の岩田です。 Lambdaの実行環境は
- EC2モデル
- Firecrackerモデル
2つのモデルに分かれることが知られていますが、Lambdaがどちらのモデル上で実行されているかを判断できるかもしれない方法を発見したのでご紹介します。
筆者の想像を多分に含む内容なので、実際の環境とは異なる可能性が高いことを事前にご了承ください。
環境
今回利用した環境です
- EC2インスタンス: i3.metal
- OS: Ubuntu Server 18.04 LTS (HVM) SSD Volume Type x86_64 (ami-024a64a6685d05041)
- Firecracker: v0.17.0
EC2モデルとFirecrackerモデルの違い
まずはおさらいから。 AWS公式の資料Security Overview of AWS Lambdaに分かりやすくまとまっていますが、Lambdaの実行環境は
- EC2モデル
- Firecrackerモデル
2つのモデルに分かれます。
EC2モデル
EC2モデルはLambdaのサービス開始当初から利用されているモデルです。 EC2インスタンス上でMicroVMが稼働し、さらにMicroVMの上に構築されたサンドボックス環境内でLambdaが実行されます。EC2モデルではEC2インスタンスがAWSアカウントの境界となり、EC2インスタンスとMicroVMの関係は1:1の関係に、MicroVMとLambda実行環境は1:Nの関係になります。
Firecrackerモデル
対するFirecrackerモデルは2018年から導入されたモデルです。ベアメタルのEC2インスタンス上でFirecrackerを利用して何十万のMicroVMを稼働させるモデルです。FirecrackerモデルではMicroVMがAWSアカウント間の境界となり、MicroVMとLambda実行環境は1:1の関係になります。
両モデルの対比
EC2モデルとFIrecrackerモデルの対比は以下の画像のようになります。
※Security Overview of AWS Lambdaより引用
この辺りの解説はこちらの記事もご参照下さい。 2019年VPC Lambdaが高速に!! AWS Lambdaの内部構造に迫るセッション 「SRV409 A Serverless Journey: AWS Lambda Under the Hood」 #reinvent
EC2モデルとFirecrackerモデルを判別できるかもしれない方法
ここからが本題です。ある日Lambda実行環境でOSコマンドを叩きながら色々分析していたところ、タイミング次第でfindmnt
コマンドの出力結果が変わることに気づきました。
パターン1
TARGET SOURCE / /dev/root[/opt/amazon/asc/worker/rtfs/cache/<ランダムな文字列>.flat] ├─/var/task /dev/loop1 ├─/dev /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev] ├─/tmp /dev/loop0 ├─/proc none │ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-<ランダムな文字列>] ├─/etc/passwd /dev/root[/etc/passwd] ├─/var/runtime /dev/root[/opt/amazon/asc/worker/runtime/nodejs-8.x] ├─/var/lang /dev/root[/opt/amazon/asc/worker/lang/node-v8.10.x] └─/etc/resolv.conf /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.conf<ランダムな文字列>]
パターン2
TARGET SOURCE / /dev/root[/opt/amazon/asc/worker/rtfs/cache/<ランダムな文字列>.flat] ├─/var/task /dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>] ├─/dev /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev] ├─/tmp /dev/loop0 ├─/proc none │ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-<ランダムな文字列>] ├─/etc/passwd /dev/root[/etc/passwd] ├─/var/runtime /dev/root[/opt/amazon/asc/worker/runtime/nodejs-8.x] ├─/var/lang /dev/root[/opt/amazon/asc/worker/lang/node-v8.10.x] └─/etc/resolv.conf /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.conf<ランダムな文字列>]
注目して欲しいのが/var/task
のマウントソースです。
/dev/loop1
の場合と/dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>]
の場合と2パターンに分かれるんです!!
最初はLambdaのランタイムによって変わるのかと思い、ランタイムを変えたりレイヤーを付け外ししたり色々試したのですが、ランタイムは同じでもタイミング次第で/var/task
のマウントソースが変わることを発見しました。これ、何かLambdaの実行モデルによる違いっぽくないですか??
EC2モデルと違ってFirecrackerモデルは1つのベアメタルインスタンス内に複数のAWSアカウントが同居するので、ベアメタルインスタンス上ではAWSアカウントごとにディレクトリを分けてるのでは? と考えました。
実際にFirecrackerを触ってみる
EC2モデルの裏側がどのようにMicroVMを構成しているか分かりませんが、Firecrackerに関してはOSSで公開されているので、実際に環境を構築しながら分析してみます。
環境構築
以下の記事を参考に執筆時点の最新バージョンFirecracker v0.17.0の環境を構築します。 Firecrackerをさわって大量のmicroVMを立ち上げてみた #reinvent
ベアメタルインスタンス上のディレクトリをMicroVMにマウントしてみる
FirecrackerのAPIを調べたところCreates or updates a drive.
というAPIでMicroVM上に新しいドライブを作成できるようです。
ベアメタルインスタンス上で以下のコマンドを入力し、MicroVMにマウントするためのファイルを作成します。
$ sudo mkdir -p /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/ $ sudo chown ubuntu:ubuntu -R /opt/amazon/ $ truncate -s 50M /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a $ mkfs.ext4 /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a
準備ができたのでMicroVMの起動準備を行います
$ ./firecracker-v0.17.0 --api-sock /tmp/firecracker.sock
$ curl --unix-socket /tmp/firecracker.sock -i \ -X PUT 'http://localhost/boot-source' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "kernel_image_path": "./hello-vmlinux.bin", "boot_args": "console=ttyS0 reboot=k panic=1 pci=off" }' HTTP/1.1 204 No Content Date: Sun, 16 Jun 2019 14:29:23 GMT
$ curl --unix-socket /tmp/firecracker.sock -i \ -X PUT 'http://localhost/drives/rootfs' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "drive_id": "rootfs", "path_on_host": "./hello-rootfs.ext4", "is_root_device": true, "is_read_only": false }' HTTP/1.1 204 No Content Date: Sun, 16 Jun 2019 14:29:40 GMT
先ほど作成したファイルをマウントするためにCreates or updates a drive.
APIを実行します
$ curl --unix-socket /tmp/firecracker.sock -i \ -X PUT 'http://localhost/drives/scratch' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "drive_id": "scratch", "path_on_host": "/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a", "is_root_device": false, "is_read_only": true }' HTTP/1.1 204 No Content Date: Sun, 16 Jun 2019 14:29:56 GMT
MicroVMを起動します
$ curl --unix-socket /tmp/firecracker.sock -i \ -X PUT 'http://localhost/actions' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "action_type": "InstanceStart" }' HTTP/1.1 204 No Content Date: Sun, 16 Jun 2019 14:30:10 GMT
MicroVMが起動したら、MicroVM内で先ほどのファイルをマウントしてfindmnt
を叩いてみます。
$ mkdir /var/task $ mount -t ext4 /dev/vdb /var/task $ findmnt TARGET SOURCE FSTYPE OPTIONS / /dev/vda ext4 rw,relatime,data=ordered ├─/dev devtmpfs devtmpf rw,nosuid,relatime,size=10240k,nr_i │ ├─/dev/mqueue mqueue mqueue rw,nosuid,nodev,noexec,relatime │ ├─/dev/pts devpts devpts rw,nosuid,noexec,relatime,gid=5,mod │ └─/dev/shm shm tmpfs rw,nosuid,nodev,noexec,relatime ├─/proc proc proc rw,nosuid,nodev,noexec,relatime │ └─/proc/sys/fs/binfmt_misc │ binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime ├─/run tmpfs tmpfs rw,nodev,relatime,size=11496k,mode= ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/security securityfs securit rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/fs/selinux selinuxfs selinux rw,relatime │ └─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime └─/var/task /dev/vdb ext4 ro,relatime,data=ordered
/var/task
のソースは/dev/vdb
となっています。そうです。。。
ベアメタルインスタンス上のファイルパスはMicroVM内では分からないのです。仮想的なデバイスとして渡してるので、当然ですね。。
ベアメタルインスタンス上はAWSアカウントでディレクトリを分けずに試してみる
ちょっと考え方を変えてやってみます。まずベアメタルインスタンス上にディスクイメージを作成し、最初に確認したディレクトリ構造を作成します。
$ truncate -s 50M /home/ubuntu/microvmdisk.img $ sudo mkfs.ext4 /home/ubuntu/microvmdisk.img $ sudo mount -t ext4 /home/ubuntu/microvmdisk.img /mnt $ sudo mkdir -p /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a $ sudo sh -c "echo hoge > /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a/hoge.txt" $ sudo umount /mnt
今度はこのイメージをMicroVMにマウントしてみます。先ほどと同様の手順でドライブの指定だけ以下のコマンドに変更してMicroVMを起動します。
$ curl --unix-socket /tmp/firecracker.sock -i \ -X PUT 'http://localhost/drives/scratch' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "drive_id": "scratch", "path_on_host": "/home/ubuntu/microvmdisk.img", "is_root_device": false, "is_read_only": true }' HTTP/1.1 204 No Content Date: Sun, 16 Jun 2019 14:42:26 GMT
MicroVMが起動できたら仮想ディスクをマウントします。
$ mount -t ext4 /dev/vdb /mnt $ mount --bind /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a /var/task $ umount /mnt
この状態でfindmnt
を叩いてみます。
$ findmnt TARGET SOURCE FSTYPE OPTIONS / /dev/vda ext4 rw,relatime,data=ordered ├─/dev devtmpfs devtmpf rw,nosuid,relatime,size=10240k,nr_i │ ├─/dev/mqueue mqueue mqueue rw,nosuid,nodev,noexec,relatime │ ├─/dev/pts devpts devpts rw,nosuid,noexec,relatime,gid=5,mod │ └─/dev/shm shm tmpfs rw,nosuid,nodev,noexec,relatime ├─/proc proc proc rw,nosuid,nodev,noexec,relatime │ └─/proc/sys/fs/binfmt_misc │ binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime ├─/run tmpfs tmpfs rw,nodev,relatime,size=11496k,mode= ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/security securityfs securit rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/fs/selinux selinuxfs selinux rw,relatime │ └─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime └─/var/task /dev/vdb[/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a] ext4 ro,relatime,data=ordered
/var/task
に関してLambda実行環境と同様の出力が得られました。
一応ファイルの中身を確認します。
$ cat /var/task/hoge.txt hoge
ベアメタルインスタンス上で書き込んだhogeという文字列が見えています。
一応MicroVM内のコンテナをシミュレートするためにMicroVM内で新しいネームスペースでシェルを起動して確認します。
unshare --mount /bin/ash
$ findmnt TARGET SOURCE FSTYPE OPTIONS / /dev/vda ext4 rw,relatime,data=ordered ├─/dev devtmpfs devtmpf rw,nosuid,relatime,size=10240k,nr_i │ ├─/dev/mqueue mqueue mqueue rw,nosuid,nodev,noexec,relatime │ ├─/dev/pts devpts devpts rw,nosuid,noexec,relatime,gid=5,mod │ └─/dev/shm shm tmpfs rw,nosuid,nodev,noexec,relatime ├─/proc proc proc rw,nosuid,nodev,noexec,relatime │ └─/proc/sys/fs/binfmt_misc │ binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime ├─/run tmpfs tmpfs rw,nodev,relatime,size=11496k,mode= ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/security securityfs securit rw,nosuid,nodev,noexec,relatime │ ├─/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime │ ├─/sys/fs/selinux selinuxfs selinux rw,relatime │ └─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime └─/var/task /dev/vdb[/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a] ext4 ro,relatime,data=ordered
OKですね。
結局Lambda実行環境はどうなの??
一応findmnt
の出力結果をLambda実行環境と同等にすることができました。が、普通に考えたらMicroVMにマウントするイメージファイル内でAWSアカウントごとにディレクトリを分けずに、MicroVMにマウントするイメージファイルそのものをAWSアカウントで分けますよね?さらにもっと言えば別にFirecracker環境でも/var/task
のマウントソースが/dev/loop1
になるような環境も簡単に構築できます。
ということで、Firecracker環境上でLambdaが実行されている場合、/var/task
のマウントソースが/dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>
になるという仮説を裏付けるような結果は何も出ませんでした。。。
まとめ
Lambda実行環境には/var/task
のマウントソースが
/dev/loop1
になる場合/dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>
になる場合
2つのパターンが存在することをご紹介しました。この2つのパターンの違いによってLambda実行環境がEC2モデルかFirecrackerモデルかを判別できると考えたのですが、残念ながら仮説の裏付けとなる検証結果が特に得られませんでした。とりあえず今日のところはLambda実行環境の/var/task
マウントソースが2パターンあるという紹介までです。
もし、今回紹介した内容からLambda実行環境の裏側についてピンと来た方がいれば、是非意見をお聞かせ下さい!!